package me.drton.flightplot;
import me.drton.flightplot.export.GPXTrackExporter;
import me.drton.flightplot.export.KMLTrackExporter;
import me.drton.flightplot.export.TrackExportDialog;
import me.drton.flightplot.export.TrackExporter;
import me.drton.flightplot.processors.PlotProcessor;
import me.drton.flightplot.processors.ProcessorsList;
import me.drton.flightplot.processors.Simple;
import me.drton.jmavlib.log.FormatErrorException;
import me.drton.jmavlib.log.LogReader;
import me.drton.jmavlib.log.px4.PX4LogReader;
import me.drton.jmavlib.log.ulog.ULogReader;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.event.ChartChangeEvent;
import org.jfree.chart.event.ChartChangeEventType;
import org.jfree.chart.event.ChartChangeListener;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.AbstractRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.Range;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.Layer;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.TextAnchor;
import org.json.JSONObject;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.*;
import java.awt.geom.Ellipse2D;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.text.NumberFormat;
import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
/**
* User: ton Date: 03.06.13 Time: 23:24
*/
public class FlightPlot {
private static final int TIME_MODE_LOG_START = 0;
private static final int TIME_MODE_BOOT = 1;
private static final int TIME_MODE_GPS = 2;
private static final NumberFormat doubleNumberFormat = NumberFormat.getInstance(Locale.ROOT);
static {
doubleNumberFormat.setGroupingUsed(false);
doubleNumberFormat.setMinimumFractionDigits(1);
doubleNumberFormat.setMaximumFractionDigits(10);
}
private static String appName = "FlightPlot";
private static String version = "0.2.25";
private static String appNameAndVersion = appName + " v." + version;
private static String colorParamPrefix = "Color ";
private final Preferences preferences;
private JFrame mainFrame;
private JLabel statusLabel;
private JPanel mainPanel;
private JTable parametersTable;
private DefaultTableModel parametersTableModel;
private ChartPanel chartPanel;
private JTable processorsList;
private DefaultTableModel processorsListModel;
private TableModelListener parameterChangedListener;
private JButton addProcessorButton;
private JButton removeProcessorButton;
private JButton openLogButton;
private JButton fieldsListButton;
private JComboBox<Preset> presetComboBox;
private List<Preset> presetsList = new ArrayList<Preset>();
private JButton deletePresetButton;
private JButton logInfoButton;
private JCheckBox markerCheckBox;
private JButton savePresetButton;
private JRadioButtonMenuItem[] timeModeItems;
private LogReader logReader = null;
private XYSeriesCollection dataset;
private JFreeChart chart;
private ColorSupplier colorSupplier;
private ProcessorsList processorsTypesList;
private File lastPresetDirectory = null;
private AddProcessorDialog addProcessorDialog;
private FieldsListDialog fieldsListDialog;
private LogInfo logInfo;
private JFileChooser openLogFileChooser;
private FileNameExtensionFilter presetExtensionFilter = new FileNameExtensionFilter("FlightPlot Presets (*.fplot)",
"fplot");
private FileNameExtensionFilter parametersExtensionFilter = new FileNameExtensionFilter("Parameters (*.txt)", "txt");
private AtomicBoolean invokeProcessFile = new AtomicBoolean(false);
private TrackExportDialog trackExportDialog;
private PlotExportDialog plotExportDialog;
private NumberAxis domainAxisSeconds;
private DateAxis domainAxisDate;
private int timeMode = 0;
private List<Map<String, Integer>> seriesIndex = new ArrayList<Map<String, Integer>>();
private ProcessorPreset editingProcessor = null;
private List<ProcessorPreset> activeProcessors = new ArrayList<ProcessorPreset>();
private Range lastTimeRange = null;
private String currentPreset = null;
public FlightPlot() {
Map<String, TrackExporter> exporters = new LinkedHashMap<String, TrackExporter>();
for (TrackExporter exporter : new TrackExporter[]{
new KMLTrackExporter(),
new GPXTrackExporter()
}) {
exporters.put(exporter.getName(), exporter);
}
trackExportDialog = new TrackExportDialog(exporters);
plotExportDialog = new PlotExportDialog(this);
preferences = Preferences.userRoot().node(appName);
mainFrame = new JFrame(appNameAndVersion);
mainFrame.setContentPane(mainPanel);
mainFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
mainFrame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
onQuit();
}
});
mainFrame.setDropTarget(new DropTarget() {
public synchronized void drop(DropTargetDropEvent evt) {
try {
evt.acceptDrop(DnDConstants.ACTION_COPY);
List<File> droppedFiles = (List<File>) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
if (droppedFiles.size() == 1) {
File file = droppedFiles.get(0);
openLog(file.getAbsolutePath());
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
createMenuBar();
java.util.List<String> processors = new ArrayList<String>(processorsTypesList.getProcessorsList());
Collections.sort(processors);
addProcessorDialog = new AddProcessorDialog(processors.toArray(new String[processors.size()]));
addProcessorDialog.pack();
fieldsListDialog = new FieldsListDialog(new Runnable() {
@Override
public void run() {
StringBuilder fieldsValue = new StringBuilder();
for (String field : fieldsListDialog.getSelectedFields()) {
if (fieldsValue.length() > 0) {
fieldsValue.append(" ");
}
fieldsValue.append(field);
}
PlotProcessor processor = new Simple();
processor.setParameters(Collections.<String, Object>singletonMap("Fields", fieldsValue.toString()));
ProcessorPreset pp = new ProcessorPreset("New", processor.getProcessorType(),
processor.getParameters(), Collections.<String, Color>emptyMap());
updatePresetParameters(pp, null);
int i = processorsListModel.getRowCount();
processorsListModel.addRow(new Object[]{true, pp});
processorsList.getSelectionModel().setSelectionInterval(i, i);
processorsList.repaint();
updateUsedColors();
showAddProcessorDialog(true);
processFile();
}
});
logInfo = new LogInfo();
addProcessorButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
showAddProcessorDialog(false);
}
});
removeProcessorButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
removeSelectedProcessor();
}
});
openLogButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
showOpenLogDialog();
}
});
fieldsListButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
fieldsListDialog.setVisible(true);
}
});
logInfoButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
logInfo.setVisible(true);
}
});
processorsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
processorsList.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent listSelectionEvent) {
// If processor changed during editing skip this event to avoid inconsistent editor state
if (editingProcessor == null) {
showProcessorParameters();
}
}
});
processorsList.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "Enter");
processorsList.getActionMap().put("Enter", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent ae) {
showAddProcessorDialog(true);
}
});
processorsList.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
JTable target = (JTable) e.getSource();
if (e.getClickCount() > 1 && target.getSelectedColumn() == 1) {
showAddProcessorDialog(true);
}
}
});
processorsListModel.addTableModelListener(new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
if (e.getType() == TableModelEvent.UPDATE) {
if (e.getColumn() == 0) {
processFile();
}
}
}
});
parameterChangedListener = new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
if (e.getType() == TableModelEvent.UPDATE) {
int row = e.getFirstRow();
onParameterChanged(row);
editingProcessor = null;
}
}
};
parametersTableModel.addTableModelListener(parameterChangedListener);
// Open Log Dialog
FileNameExtensionFilter[] logExtensionfilters = new FileNameExtensionFilter[]{
new FileNameExtensionFilter("PX4/APM Log (*.px4log, *.bin)", "px4log", "bin"),
new FileNameExtensionFilter("ULog (*.ulg)", "ulg")
};
openLogFileChooser = new JFileChooser();
for (FileNameExtensionFilter filter : logExtensionfilters) {
openLogFileChooser.addChoosableFileFilter(filter);
}
openLogFileChooser.setFileFilter(logExtensionfilters[0]);
openLogFileChooser.setDialogTitle("Open Log");
presetComboBox.setMaximumRowCount(30);
presetComboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
onPresetAction(e);
}
});
updatePresetEdited(true);
savePresetButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
onSavePreset();
}
});
deletePresetButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
onDeletePreset();
}
});
markerCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
setChartMarkers();
}
});
mainFrame.pack();
mainFrame.setVisible(true);
// Load preferences
try {
loadPreferences();
} catch (BackingStoreException e) {
e.printStackTrace();
}
}
public static void main(String[] args)
throws ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException,
IllegalAccessException {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (OSValidator.isMac()) {
System.setProperty("apple.laf.useScreenMenuBar", "true");
}
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
e.printStackTrace();
return;
}
new FlightPlot();
}
});
}
private static Object formatParameterValue(Object value) {
Object returnValue;
if (value instanceof Double) {
returnValue = doubleNumberFormat.format(value);
} else if (value instanceof Color) {
returnValue = value;
} else {
returnValue = value.toString();
}
return returnValue;
}
private void onQuit() {
savePreferences();
System.exit(0);
}
private void onPresetAction(ActionEvent e) {
if ("comboBoxEdited".equals(e.getActionCommand())) {
// Save preset
onSavePreset();
} else if ("comboBoxChanged".equals(e.getActionCommand())) {
// Load preset
String oldPreset = currentPreset;
Object selection = presetComboBox.getSelectedItem();
if (selection == null) {
processorsListModel.setRowCount(0);
updateUsedColors();
currentPreset = null;
} else if (selection instanceof Preset) {
loadPreset((Preset) selection);
currentPreset = ((Preset) selection).getTitle();
}
updatePresetEdited(false);
if ((currentPreset == null && oldPreset != null) || (currentPreset != null && !currentPreset.equals(oldPreset))) {
processFile();
}
}
}
private void onSavePreset() {
String presetTitle = presetComboBox.getSelectedItem().toString();
if (presetTitle.isEmpty()) {
setStatus("Enter preset name first");
return;
}
Preset preset = formatPreset(presetTitle);
boolean addNew = true;
for (int i = 0; i < presetsList.size(); i++) {
if (presetTitle.equals(presetsList.get(i).getTitle())) {
// Update existing preset
addNew = false;
presetsList.set(i, preset);
setStatus("Preset \"" + preset.getTitle() + "\" updated");
break;
}
}
if (addNew) {
// Add new preset
presetsList.add(preset);
currentPreset = preset.getTitle();
setStatus("Preset \"" + preset.getTitle() + "\" added");
}
loadPresetsList();
updatePresetEdited(false);
savePreferences();
}
private void onDeletePreset() {
int i = presetComboBox.getSelectedIndex();
Preset removedPreset = null;
if (i > 0) {
removedPreset = presetsList.remove(i - 1);
}
if (removedPreset != null) {
loadPresetsList();
setStatus("Preset \"" + removedPreset.getTitle() + "\" deleted");
savePreferences();
}
}
private void updatePresetEdited(boolean edited) {
presetComboBox.getEditor().getEditorComponent().setForeground(edited ? Color.GRAY : Color.BLACK);
}
private void loadPreferences() throws BackingStoreException {
PreferencesUtil.loadWindowPreferences(mainFrame, preferences.node("MainWindow"), 800, 600);
PreferencesUtil.loadWindowPreferences(fieldsListDialog, preferences.node("FieldsListDialog"), 300, 600);
PreferencesUtil.loadWindowPreferences(addProcessorDialog, preferences.node("AddProcessorDialog"), -1, -1);
PreferencesUtil.loadWindowPreferences(logInfo.getFrame(), preferences.node("LogInfoFrame"), 600, 600);
String logDirectoryStr = preferences.get("LogDirectory", null);
if (logDirectoryStr != null) {
File dir = new File(logDirectoryStr);
openLogFileChooser.setCurrentDirectory(dir);
}
String presetDirectoryStr = preferences.get("PresetDirectory", null);
if (presetDirectoryStr != null) {
lastPresetDirectory = new File(presetDirectoryStr);
}
Preferences presets = preferences.node("Presets");
presetsList.clear();
for (String p : presets.keys()) {
try {
Preset preset = Preset.unpackJSONObject(new JSONObject(presets.get(p, "{}")));
if (preset != null) {
presetsList.add(preset);
}
} catch (Exception e) {
e.printStackTrace();
}
}
loadPresetsList();
timeMode = Integer.parseInt(preferences.get("TimeMode", "0"));
timeModeItems[timeMode].setSelected(true);
markerCheckBox.setSelected(preferences.getBoolean("ShowMarkers", false));
trackExportDialog.loadPreferences(preferences);
plotExportDialog.loadPreferences(preferences);
}
private void loadPresetsList() {
Comparator<Preset> presetComparator = new Comparator<Preset>() {
@Override
public int compare(Preset o1, Preset o2) {
return o1.getTitle().compareToIgnoreCase(o2.getTitle());
}
};
Collections.sort(presetsList, presetComparator);
presetComboBox.removeAllItems();
presetComboBox.addItem(null);
Preset selectPreset = null;
for (Preset preset : presetsList) {
presetComboBox.addItem(preset);
if (preset.getTitle().equals(currentPreset)) {
selectPreset = preset;
}
}
presetComboBox.setSelectedItem(selectPreset);
}
private void savePreferences() {
try {
preferences.clear();
for (String child : preferences.childrenNames()) {
preferences.node(child).removeNode();
}
PreferencesUtil.saveWindowPreferences(mainFrame, preferences.node("MainWindow"));
PreferencesUtil.saveWindowPreferences(fieldsListDialog, preferences.node("FieldsListDialog"));
PreferencesUtil.saveWindowPreferences(addProcessorDialog, preferences.node("AddProcessorDialog"));
PreferencesUtil.saveWindowPreferences(logInfo.getFrame(), preferences.node("LogInfoFrame"));
File lastLogDirectory = openLogFileChooser.getCurrentDirectory();
if (lastLogDirectory != null) {
preferences.put("LogDirectory", lastLogDirectory.getAbsolutePath());
}
if (lastPresetDirectory != null) {
preferences.put("PresetDirectory", lastPresetDirectory.getAbsolutePath());
}
Preferences presetsPref = preferences.node("Presets");
for (int i = 0; i < presetComboBox.getItemCount(); i++) {
Object object = presetComboBox.getItemAt(i);
if (object != null) {
Preset preset = (Preset) object;
try {
presetsPref.put(preset.getTitle(), preset.packJSONObject().toString());
} catch (IOException e) {
e.printStackTrace();
}
}
}
preferences.put("TimeMode", Integer.toString(timeMode));
preferences.putBoolean("ShowMarkers", markerCheckBox.isSelected());
trackExportDialog.savePreferences(preferences);
plotExportDialog.savePreferences(preferences);
preferences.sync();
} catch (BackingStoreException e) {
e.printStackTrace();
}
}
private void loadPreset(Preset preset) {
processorsListModel.setRowCount(0);
for (ProcessorPreset pp : preset.getProcessorPresets()) {
updatePresetParameters(pp, null);
processorsListModel.addRow(new Object[]{true, pp.clone()});
}
updateUsedColors();
}
private Preset formatPreset(String title) {
List<ProcessorPreset> processorPresets = new ArrayList<ProcessorPreset>();
for (int i = 0; i < processorsListModel.getRowCount(); i++) {
processorPresets.add(((ProcessorPreset) processorsListModel.getValueAt(i, 1)).clone());
}
return new Preset(title, processorPresets);
}
private void createUIComponents() throws IllegalAccessException, InstantiationException {
// Chart panel
processorsTypesList = new ProcessorsList();
dataset = new XYSeriesCollection();
colorSupplier = new ColorSupplier();
chart = ChartFactory.createXYLineChart("", "", "", null, PlotOrientation.VERTICAL, true, true, false);
chart.getXYPlot().setDataset(dataset);
// Set plot colors
XYPlot plot = chart.getXYPlot();
plot.setBackgroundPaint(Color.WHITE);
plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
final XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(true, false);
plot.setRenderer(renderer);
// Domain (X) axis - seconds
domainAxisSeconds = new NumberAxis("T") {
// Use default auto range to adjust range
protected void autoAdjustRange() {
setRange(getDefaultAutoRange());
}
};
//domainAxisSeconds.setAutoRangeIncludesZero(false);
domainAxisSeconds.setLowerMargin(0.0);
domainAxisSeconds.setUpperMargin(0.0);
// Domain (X) axis - date
domainAxisDate = new DateAxis("T") {
// Use default auto range to adjust range
protected void autoAdjustRange() {
setRange(getDefaultAutoRange());
}
};
domainAxisDate.setTimeZone(TimeZone.getTimeZone("GMT"));
domainAxisDate.setLowerMargin(0.0);
domainAxisDate.setUpperMargin(0.0);
// Use seconds by default
plot.setDomainAxis(domainAxisSeconds);
// Range (Y) axis
NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
rangeAxis.setAutoRangeIncludesZero(false);
chartPanel = new ChartPanel(chart, false);
chartPanel.setMouseWheelEnabled(true);
chartPanel.setMouseZoomable(true, false);
chartPanel.setPopupMenu(null);
chart.addChangeListener(new ChartChangeListener() {
@Override
public void chartChanged(ChartChangeEvent chartChangeEvent) {
if (chartChangeEvent.getType() == ChartChangeEventType.GENERAL) {
Range timeRange = chart.getXYPlot().getDomainAxis().getRange();
if (!timeRange.equals(lastTimeRange)) {
lastTimeRange = timeRange;
processFile();
}
}
}
});
// Processors list
processorsListModel = new DefaultTableModel() {
@Override
public boolean isCellEditable(int row, int col) {
return col == 0;
}
@Override
public Class<?> getColumnClass(int col) {
return col == 0 ? Boolean.class : String.class;
}
};
processorsListModel.addColumn("");
processorsListModel.addColumn("Processor");
processorsList = new JTable(processorsListModel);
processorsList.getColumnModel().getColumn(0).setMinWidth(20);
processorsList.getColumnModel().getColumn(0).setMaxWidth(20);
// Parameters table
parametersTableModel = new DefaultTableModel() {
@Override
public boolean isCellEditable(int row, int col) {
return col == 1;
}
};
parametersTableModel.addColumn("Parameter");
parametersTableModel.addColumn("Value");
parametersTable = new JTable(parametersTableModel);
parametersTable.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "startEditing");
parametersTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
parametersTable.getColumnModel().getColumn(1).setCellEditor(new ParamValueTableCellEditor(this));
parametersTable.getColumnModel().getColumn(1).setCellRenderer(new ParamValueTableCellRenderer());
parametersTable.putClientProperty("JTable.autoStartsEdit", false);
parametersTable.putClientProperty("terminateEditOnFocusLost", true);
}
private void createMenuBar() {
// File menu
JMenu fileMenu = new JMenu("File");
JMenuItem fileOpenItem = new JMenuItem("Open Log...");
fileOpenItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
showOpenLogDialog();
}
});
fileMenu.add(fileOpenItem);
JMenuItem importPresetItem = new JMenuItem("Import Preset...");
importPresetItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
showImportPresetDialog();
}
});
fileMenu.add(importPresetItem);
JMenuItem exportPresetItem = new JMenuItem("Export Preset...");
exportPresetItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
showExportPresetDialog();
}
});
fileMenu.add(exportPresetItem);
JMenuItem exportAsImageItem = new JMenuItem("Export As Image...");
exportAsImageItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
showExportAsImageDialog();
}
});
fileMenu.add(exportAsImageItem);
JMenuItem exportTrackItem = new JMenuItem("Export Track...");
exportTrackItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
showExportTrackDialog();
}
});
fileMenu.add(exportTrackItem);
JMenuItem exportParametersItem = new JMenuItem("Export Parameters...");
exportParametersItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
showExportParametersDialog();
}
});
fileMenu.add(exportParametersItem);
if (!OSValidator.isMac()) {
fileMenu.add(new JPopupMenu.Separator());
JMenuItem exitItem = new JMenuItem("Exit");
exitItem.setAccelerator(KeyStroke.getKeyStroke('Q', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
exitItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
onQuit();
}
});
fileMenu.add(exitItem);
}
// View menu
JMenu viewMenu = new JMenu("View");
timeModeItems = new JRadioButtonMenuItem[3];
timeModeItems[TIME_MODE_LOG_START] = new JRadioButtonMenuItem("Log Start Time");
timeModeItems[TIME_MODE_BOOT] = new JRadioButtonMenuItem("Boot Time");
timeModeItems[TIME_MODE_GPS] = new JRadioButtonMenuItem("GPS Time");
ButtonGroup timeModeGroup = new ButtonGroup();
for (JRadioButtonMenuItem item : timeModeItems) {
timeModeGroup.add(item);
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
onTimeModeChanged();
processFile();
}
});
viewMenu.add(item);
}
// Menu bar
JMenuBar menuBar = new JMenuBar();
menuBar.add(fileMenu);
menuBar.add(viewMenu);
mainFrame.setJMenuBar(menuBar);
}
private void onTimeModeChanged() {
int timeModeOld = timeMode;
for (int i = 0; i < timeModeItems.length; i++) {
if (timeModeItems[i].isSelected()) {
timeMode = i;
break;
}
}
long timeOffset = 0;
long logStart = 0;
long logSize = 1000000;
Range rangeOld = new Range(0.0, 1.0);
if (logReader != null) {
timeOffset = getTimeOffset(timeMode);
logStart = logReader.getStartMicroseconds() + timeOffset;
logSize = logReader.getSizeMicroseconds();
rangeOld = getLogRange(timeModeOld);
}
ValueAxis domainAxis = selectDomainAxis(timeMode);
// Set axis type according to selected time mode
chart.getXYPlot().setDomainAxis(0, domainAxis, false);
if (domainAxis == domainAxisDate) {
// DateAxis uses ms instead of seconds
domainAxis.setRange(new Range(rangeOld.getLowerBound() * 1e3 + timeOffset * 1e-3,
rangeOld.getUpperBound() * 1e3 + timeOffset * 1e-3), true, false);
domainAxis.setDefaultAutoRange(new Range(logStart * 1e-3, (logStart + logSize) * 1e-3));
} else {
domainAxis.setRange(new Range(rangeOld.getLowerBound() + timeOffset * 1e-6,
rangeOld.getUpperBound() + timeOffset * 1e-6), true, false);
domainAxis.setDefaultAutoRange(new Range(logStart * 1e-6, (logStart + logSize) * 1e-6));
}
}
/**
* Displayed log range in seconds of native log time
*
* @param tm time mode
* @return displayed log range [s]
*/
private Range getLogRange(int tm) {
Range range = selectDomainAxis(tm).getRange();
if (tm == TIME_MODE_GPS) {
long timeOffset = getTimeOffset(tm);
return new Range((range.getLowerBound() * 1e3 - timeOffset) * 1e-6,
(range.getUpperBound() * 1e3 - timeOffset) * 1e-6);
} else {
long timeOffset = getTimeOffset(tm);
return new Range(range.getLowerBound() - timeOffset * 1e-6, range.getUpperBound() - timeOffset * 1e-6);
}
}
public void setStatus(String status) {
statusLabel.setText(status);
}
public void showOpenLogDialog() {
int returnVal = openLogFileChooser.showDialog(mainFrame, "Open");
if (returnVal == JFileChooser.APPROVE_OPTION) {
File file = openLogFileChooser.getSelectedFile();
String logFileName = file.getPath();
openLog(logFileName);
}
}
private void openLog(String logFileName) {
String logFileNameLower = logFileName.toLowerCase();
LogReader logReaderNew;
try {
if (logFileNameLower.endsWith(".bin") || logFileNameLower.endsWith(".px4log")) {
logReaderNew = new PX4LogReader(logFileName);
} else if (logFileNameLower.endsWith(".ulg")) {
logReaderNew = new ULogReader(logFileName);
} else {
setStatus("Log format not supported: " + logFileName);
return;
}
} catch (Exception e) {
setStatus("Error: " + e);
e.printStackTrace();
return;
}
mainFrame.setTitle(appNameAndVersion + " - " + logFileName);
if (logReader != null) {
try {
logReader.close();
} catch (IOException e) {
e.printStackTrace();
}
logReader = null;
}
logReader = logReaderNew;
if (logReader.getErrors().size() > 0) {
setStatus("Log file opened: " + logFileName + " (errors: " + logReader.getErrors().size() + ", see console output)");
printLogErrors();
} else {
setStatus("Log file opened: " + logFileName);
}
logInfo.updateInfo(logReader);
fieldsListDialog.setFieldsList(logReader.getFields());
onTimeModeChanged();
chart.getXYPlot().getDomainAxis().setAutoRange(true);
chart.getXYPlot().getRangeAxis().setAutoRange(true);
processFile();
}
public void showImportPresetDialog() {
JFileChooser fc = new JFileChooser();
if (lastPresetDirectory != null) {
fc.setCurrentDirectory(lastPresetDirectory);
}
fc.setFileFilter(presetExtensionFilter);
fc.setDialogTitle("Import Preset");
int returnVal = fc.showDialog(mainFrame, "Import");
if (returnVal == JFileChooser.APPROVE_OPTION) {
lastPresetDirectory = fc.getCurrentDirectory();
File file = fc.getSelectedFile();
try {
byte[] b = new byte[(int) file.length()];
FileInputStream fileInputStream = new FileInputStream(file);
int n = 0;
while (n < b.length) {
int r = fileInputStream.read(b, n, b.length - n);
if (r <= 0) {
throw new Exception("Read error");
}
n += r;
}
Preset preset = Preset.unpackJSONObject(new JSONObject(new String(b, Charset.forName("utf8"))));
loadPreset(preset);
processFile();
} catch (Exception e) {
setStatus("Error: " + e);
e.printStackTrace();
}
}
}
public void showExportPresetDialog() {
JFileChooser fc = new JFileChooser();
if (lastPresetDirectory != null) {
fc.setCurrentDirectory(lastPresetDirectory);
}
fc.setFileFilter(presetExtensionFilter);
fc.setDialogTitle("Export Preset");
int returnVal = fc.showDialog(mainFrame, "Export");
if (returnVal == JFileChooser.APPROVE_OPTION) {
lastPresetDirectory = fc.getCurrentDirectory();
String fileName = fc.getSelectedFile().toString();
if (presetExtensionFilter == fc.getFileFilter() && !fileName.toLowerCase().endsWith(".fplot")) {
fileName += ".fplot";
}
try {
Object item = presetComboBox.getSelectedItem();
String presetTitle = item == null ? "" : item.toString();
Preset preset = formatPreset(presetTitle);
FileWriter fileWriter = new FileWriter(new File(fileName));
fileWriter.write(preset.packJSONObject().toString(1));
fileWriter.close();
setStatus("Preset saved to: " + fileName);
} catch (Exception e) {
setStatus("Error: " + e);
e.printStackTrace();
}
}
}
public void showExportAsImageDialog() {
if (logReader == null) {
JOptionPane.showMessageDialog(mainFrame, "Log file must be opened first.", "Error",
JOptionPane.ERROR_MESSAGE);
return;
}
try {
plotExportDialog.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
showExportTrackStatusMessage("Track could not be exported.");
}
}
public void showExportTrackDialog() {
if (logReader == null) {
JOptionPane.showMessageDialog(mainFrame, "Log file must be opened first.", "Error",
JOptionPane.ERROR_MESSAGE);
return;
}
try {
trackExportDialog.display(logReader, getLogRange(timeMode));
} catch (Exception e) {
e.printStackTrace();
showExportTrackStatusMessage("Track could not be exported.");
}
}
private void showExportTrackStatusMessage(String message) {
setStatus(String.format("Track export: %s", message));
}
public void showExportParametersDialog() {
if (logReader == null) {
JOptionPane.showMessageDialog(mainFrame, "Log file must be opened first.", "Error",
JOptionPane.ERROR_MESSAGE);
return;
}
JFileChooser fc = new JFileChooser();
fc.setFileFilter(parametersExtensionFilter);
fc.setDialogTitle("Export Parameters");
int returnVal = fc.showDialog(mainFrame, "Export");
if (returnVal == JFileChooser.APPROVE_OPTION) {
String fileName = fc.getSelectedFile().toString();
if (parametersExtensionFilter == fc.getFileFilter() && !fileName.toLowerCase().endsWith(".txt")) {
fileName += ".txt";
}
try {
FileWriter fileWriter = new FileWriter(new File(fileName));
List<Map.Entry<String, Object>> paramsList = new ArrayList<Map.Entry<String, Object>>(logReader.getParameters().entrySet());
Collections.sort(paramsList, new Comparator<Map.Entry<String, Object>>() {
@Override
public int compare(Map.Entry<String, Object> o1, Map.Entry<String, Object> o2) {
return o1.getKey().compareTo(o2.getKey());
}
});
for (Map.Entry<String, Object> param : paramsList) {
int typeID = 0;
Object value = param.getValue();
if (value instanceof Float) {
typeID = 1;
}
fileWriter.write(String.format("%s\t%s\t%s\n", param.getKey(), typeID, param.getValue()));
}
fileWriter.close();
} catch (Exception e) {
setStatus("Error: " + e);
e.printStackTrace();
}
}
}
private void processFile() {
if (logReader != null) {
if (invokeProcessFile.compareAndSet(false, true)) {
final boolean notEmptyPlot = (getActiveProcessors().size() > 0);
if (notEmptyPlot) {
setStatus("Processing...");
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
generateSeries();
if (notEmptyPlot) {
if (logReader.getErrors().size() > 0) {
setStatus("Log parsing errors, see console output");
printLogErrors();
} else {
setStatus(" ");
}
}
} catch (Exception e) {
setStatus("Error: " + e);
e.printStackTrace();
}
invokeProcessFile.lazySet(false);
}
});
}
}
}
private void printLogErrors() {
System.err.println("Log parsing errors:");
int maxErrors = 100;
for (Exception e : logReader.getErrors().subList(0, Math.min(logReader.getErrors().size(), maxErrors))) {
System.err.println("\t" + e.getMessage());
}
if (logReader.getErrors().size() > maxErrors) {
System.err.println("\t...");
}
}
private long getTimeOffset(int tm) {
// Set time offset according t selected time mode
long timeOffset = 0;
if (tm == TIME_MODE_GPS) {
// GPS time
timeOffset = logReader.getUTCTimeReferenceMicroseconds();
if (timeOffset < 0) {
timeOffset = 0;
}
} else if (tm == TIME_MODE_LOG_START) {
// Log start time
timeOffset = -logReader.getStartMicroseconds();
}
return timeOffset;
}
private ValueAxis selectDomainAxis(int tm) {
if (tm == TIME_MODE_GPS) {
return domainAxisDate;
} else {
return domainAxisSeconds;
}
}
private List<ProcessorPreset> getActiveProcessors() {
List<ProcessorPreset> processors = new ArrayList<ProcessorPreset>();
for (int row = 0; row < processorsListModel.getRowCount(); row++) {
if ((Boolean) processorsListModel.getValueAt(row, 0)) {
processors.add((ProcessorPreset) processorsListModel.getValueAt(row, 1));
}
}
return processors;
}
private void generateSeries() throws IOException, FormatErrorException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
activeProcessors.clear();
activeProcessors.addAll(getActiveProcessors());
dataset.removeAllSeries();
seriesIndex.clear();
PlotProcessor[] processors = new PlotProcessor[activeProcessors.size()];
// Update time offset according to selected time mode
long timeOffset = getTimeOffset(timeMode);
// Displayed log range in seconds of native log time
Range range = getLogRange(timeMode);
// Process some extra data in hidden areas
long timeStart = (long) ((range.getLowerBound() - range.getLength()) * 1e6);
long timeStop = (long) ((range.getUpperBound() + range.getLength()) * 1e6);
timeStart = Math.max(logReader.getStartMicroseconds(), timeStart);
timeStop = Math.min(logReader.getStartMicroseconds() + logReader.getSizeMicroseconds(), timeStop);
double timeScale = (selectDomainAxis(timeMode) == domainAxisDate) ? 1000.0 : 1.0;
int displayPixels = 2000;
double skip = range.getLength() / displayPixels;
if (processors.length > 0) {
for (int i = 0; i < activeProcessors.size(); i++) {
ProcessorPreset pp = activeProcessors.get(i);
PlotProcessor processor;
processor = processorsTypesList.getProcessorInstance(pp, skip, logReader.getFields());
processor.setFieldsList(logReader.getFields());
processors[i] = processor;
}
logReader.seek(timeStart);
logReader.clearErrors();
Map<String, Object> data = new HashMap<String, Object>();
while (true) {
long t;
data.clear();
try {
t = logReader.readUpdate(data);
} catch (EOFException e) {
break;
}
if (t > timeStop) {
break;
}
for (PlotProcessor processor : processors) {
processor.process((t + timeOffset) * 1e-6, data);
}
}
chart.getXYPlot().clearDomainMarkers();
for (int i = 0; i < activeProcessors.size(); i++) {
PlotProcessor processor = processors[i];
String processorTitle = activeProcessors.get(i).getTitle();
Map<String, Integer> processorSeriesIndex = new HashMap<String, Integer>();
seriesIndex.add(processorSeriesIndex);
for (PlotItem item : processor.getSeriesList()) {
if (item instanceof Series) {
Series series = (Series) item;
processorSeriesIndex.put(series.getTitle(), dataset.getSeriesCount());
XYSeries jseries = new XYSeries(series.getFullTitle(processorTitle), false);
for (XYPoint point : series) {
jseries.add(point.x * timeScale, point.y, false);
}
dataset.addSeries(jseries);
} else if (item instanceof MarkersList) {
MarkersList markers = (MarkersList) item;
processorSeriesIndex.put(markers.getTitle(), dataset.getSeriesCount());
XYSeries jseries = new XYSeries(markers.getFullTitle(processorTitle), false);
dataset.addSeries(jseries);
for (Marker marker : markers) {
TaggedValueMarker m = new TaggedValueMarker(i, marker.x * timeScale);
m.setPaint(Color.black);
m.setLabel(marker.label);
m.setLabelAnchor(RectangleAnchor.TOP_RIGHT);
m.setLabelTextAnchor(TextAnchor.TOP_LEFT);
chart.getXYPlot().addDomainMarker(0, m, Layer.BACKGROUND, false);
}
}
}
}
setChartColors();
setChartMarkers();
}
chartPanel.repaint();
}
private void setChartColors() {
if (dataset.getSeriesCount() > 0) {
Collection<ValueMarker> markers = chart.getXYPlot().getDomainMarkers(0, Layer.BACKGROUND);
for (int i = 0; i < activeProcessors.size(); i++) {
for (Map.Entry<String, Integer> entry : seriesIndex.get(i).entrySet()) {
ProcessorPreset processorPreset = activeProcessors.get(i);
AbstractRenderer renderer = (AbstractRenderer) chart.getXYPlot().getRendererForDataset(dataset);
Paint color = processorPreset.getColors().get(entry.getKey());
renderer.setSeriesPaint(entry.getValue(), color, true);
if (markers != null) {
for (ValueMarker marker : markers) {
if (((TaggedValueMarker) marker).tag == i) {
marker.setPaint(color);
}
}
}
}
}
}
}
private void setChartMarkers() {
if (dataset.getSeriesCount() > 0) {
boolean showMarkers = markerCheckBox.isSelected();
Shape marker = new Ellipse2D.Double(-1.5, -1.5, 3, 3);
Object renderer = chart.getXYPlot().getRendererForDataset(dataset);
if (renderer instanceof XYLineAndShapeRenderer) {
for (int j = 0; j < dataset.getSeriesCount(); j++) {
if (showMarkers) {
((XYLineAndShapeRenderer) renderer).setSeriesShape(j, marker, false);
}
((XYLineAndShapeRenderer) renderer).setSeriesShapesVisible(j, showMarkers);
}
}
}
}
private void showAddProcessorDialog(boolean editMode) {
ProcessorPreset selectedProcessor = editMode ? getSelectedProcessor() : null;
addProcessorDialog.display(new Runnable() {
@Override
public void run() {
onAddProcessorDialogOK();
}
}, selectedProcessor);
}
private void onAddProcessorDialogOK() {
updatePresetEdited(true);
ProcessorPreset processorPreset = addProcessorDialog.getOrigProcessorPreset();
String title = addProcessorDialog.getProcessorTitle();
String processorType = addProcessorDialog.getProcessorType();
if (processorPreset != null) {
// Edit processor
ProcessorPreset processorPresetNew = processorPreset;
if (!processorPreset.getProcessorType().equals(processorType)) {
// Processor type changed
Map<String, Object> parameters = processorPreset.getParameters();
processorPresetNew = new ProcessorPreset(title, processorType, new HashMap<String, Object>(), Collections.<String, Color>emptyMap());
updatePresetParameters(processorPresetNew, parameters);
for (int row = 0; row < processorsListModel.getRowCount(); row++) {
if (processorsListModel.getValueAt(row, 1) == processorPreset) {
processorsListModel.setValueAt(processorPresetNew, row, 1);
processorsList.setRowSelectionInterval(row, row);
break;
}
}
showProcessorParameters();
} else {
// Only change title
processorPresetNew.setTitle(title);
}
} else {
processorPreset = new ProcessorPreset(title, processorType, Collections.<String, Object>emptyMap(), Collections.<String, Color>emptyMap());
updatePresetParameters(processorPreset, null);
int i = processorsListModel.getRowCount();
processorsListModel.addRow(new Object[]{true, processorPreset});
processorsList.setRowSelectionInterval(i, i);
}
updateUsedColors();
processFile();
}
private void updatePresetParameters(ProcessorPreset processorPreset, Map<String, Object> parametersUpdate) {
if (parametersUpdate != null) {
// Update parameters of preset
processorPreset.getParameters().putAll(parametersUpdate);
}
// Construct and initialize processor to cleanup parameters list and get list of series
PlotProcessor p;
try {
p = processorsTypesList.getProcessorInstance(processorPreset, 0.0, null);
} catch (Exception e) {
setStatus("Error in processor \"" + processorPreset + "\"");
e.printStackTrace();
return;
}
processorPreset.setParameters(p.getParameters());
Map<String, Color> colorsNew = new HashMap<String, Color>();
for (PlotItem series : p.getSeriesList()) {
Color color = processorPreset.getColors().get(series.getTitle());
if (color == null) {
color = colorSupplier.getNextColor(series.getTitle());
}
colorsNew.put(series.getTitle(), color);
}
processorPreset.setColors(colorsNew);
}
private void removeSelectedProcessor() {
ProcessorPreset selectedProcessor = getSelectedProcessor();
if (selectedProcessor != null) {
int row = processorsList.getSelectedRow();
processorsListModel.removeRow(row);
updatePresetEdited(true);
updateUsedColors();
processFile();
}
}
private void updateUsedColors() {
colorSupplier.resetColorsUsed();
for (int i = 0; i < processorsListModel.getRowCount(); i++) {
ProcessorPreset pp = (ProcessorPreset) processorsListModel.getValueAt(i, 1);
for (Color color : pp.getColors().values()) {
colorSupplier.markColorUsed(color);
}
}
}
private ProcessorPreset getSelectedProcessor() {
int row = processorsList.getSelectedRow();
return row < 0 ? null : (ProcessorPreset) processorsListModel.getValueAt(row, 1);
}
private void showProcessorParameters() {
while (parametersTableModel.getRowCount() > 0) {
parametersTableModel.removeRow(0);
}
ProcessorPreset selectedProcessor = getSelectedProcessor();
if (selectedProcessor != null) {
// Parameters
Map<String, Object> params = selectedProcessor.getParameters();
List<String> param_keys = new ArrayList<String>(params.keySet());
Collections.sort(param_keys);
for (String key : param_keys) {
parametersTableModel.addRow(new Object[]{key, formatParameterValue(params.get(key))});
}
// Colors
Map<String, Color> colors = selectedProcessor.getColors();
List<String> color_keys = new ArrayList<String>(colors.keySet());
Collections.sort(color_keys);
for (String key : color_keys) {
parametersTableModel.addRow(new Object[]{colorParamPrefix + key, colors.get(key)});
}
}
}
private void onParameterChanged(int row) {
if (editingProcessor != null && editingProcessor == getSelectedProcessor()) {
String key = parametersTableModel.getValueAt(row, 0).toString();
Object value = parametersTableModel.getValueAt(row, 1);
if (value instanceof Color) {
editingProcessor.getColors().put(key.substring(colorParamPrefix.length(), key.length()), (Color) value);
setChartColors();
}
try {
updatePresetParameters(editingProcessor, Collections.<String, Object>singletonMap(key, value.toString()));
updatePresetEdited(true);
} catch (Exception e) {
e.printStackTrace();
setStatus("Error: " + e);
}
if (!(value instanceof Color)) {
parametersTableModel.removeTableModelListener(parameterChangedListener);
showProcessorParameters(); // refresh all parameters because changing one param might influence others (e.g. color)
parametersTableModel.addTableModelListener(parameterChangedListener);
parametersTable.addRowSelectionInterval(row, row);
processFile();
}
}
}
ColorSupplier getColorSupplier() {
return colorSupplier;
}
void setEditingProcessor() {
editingProcessor = getSelectedProcessor();
}
public JFreeChart getChart() {
return chart;
}
}